using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Linq;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using CeskyRozhlasRT.DataModel;
using CeskyRozhlasRT.Parsing;
using Microsoft.WindowsAzure.ServiceRuntime;
using Pkcs12ProtectedConfigurationProvider;
using ScheduleParserWorkerRole;

namespace ScheduleParserWorkerRole
{

    public class ScheduleParserWorkerRole : RoleEntryPoint
    {
        private FastEncryptionWorkaround _encryptor;
        private string _connectionString;
        //In Azure, we always run in UTC, so we will need to convert date/time from CZ to UTC
        private TimeZoneInfo _crawledScheduleTimeZone;
        private SqlConnectionStringBuilder _csBuilder;
        private StationsDataModelDataContext _context;
        private readonly DataExpirationManager _expiration = new DataExpirationManager();

        public override bool OnStart()
        {
            // Set the maximum number of concurrent connections 
            ServicePointManager.DefaultConnectionLimit = 12;

            // For information on handling configuration changes
            // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357.

            bool success = base.OnStart();
            try
            {
                _crawledScheduleTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time");
                _encryptor = new FastEncryptionWorkaround
                {
                    Thumbprint = RoleEnvironment.GetConfigurationSettingValue("DecryptionCert")
                };
                _connectionString = _encryptor.Decrypt(RoleEnvironment.GetConfigurationSettingValue("DatabaseConnectionString"));
                Trace.TraceInformation("ScheduleParserWorkerRole onStart - creating connection");
                _csBuilder = new SqlConnectionStringBuilder(_connectionString);
                _context = new StationsDataModelDataContext(_csBuilder.ConnectionString);
                return success;
            }
            catch (Exception e)
            {
                Trace.TraceInformation("ScheduleParserWorkerRole onStart - failed, exc:{0}", e);
                return false;
            }
        }



        public override void Run()
        {
            Trace.TraceInformation("ScheduleParserWorkerRole entry point called", "Information");

            while (true)
            {
                try
                {
                    MainCycle();
                }
                catch (Exception e)
                {
                    Trace.TraceError("ScheduleParserWorkerRole MainCycle - failed, exc:{0}", e);
                }
            }
        }


        //There is lots of copypaste in refresh* methods. 
        //TODO: deploy known GoF patterns to reduce copypaste
        private void MainCycle()
        {
            Trace.TraceInformation("Started schedule refresh");
            //Todo: just synchronize existing state instead of overwriting it
            RefetchSchedulesFromWeb();
            SubmitToDb();
            Thread.Sleep(CalculateInactivityTimeout());
        }

        private void RefetchSchedulesFromWeb()
        {
            RefreshAllGroups();
            RefreshAllSchedules();
        }

        private void SubmitToDb()
        {
            Trace.TraceInformation("Submit Changes ");
            _context.Refresh(RefreshMode.OverwriteCurrentValues, _context.schedules);
            _context.SubmitChanges();
        }

        private TimeSpan CalculateInactivityTimeout()
        {
            var aWakeUpTime = _expiration.GetNearestExpirationTime();
            TimeSpan aSleepInterval;
            if (aWakeUpTime < DateTime.UtcNow)
            {
                aSleepInterval = TimeSpan.FromSeconds(5);
            }
            else
            {
                aSleepInterval = aWakeUpTime - DateTime.UtcNow;
            }
            Trace.TraceInformation("Finished Submit, sleeping for {0}, until {1}", aSleepInterval, aWakeUpTime);
            return aSleepInterval;
        }

        private void RefreshAllGroups()
        {
            foreach (var iterGroup in _context.groups)
            {
                RefreshGroup(iterGroup);
            }
        }

        private void RefreshGroup(group iGroup)
        {
            try
            {
                if (_expiration.IsExpired(iGroup.Name))
                {
                    RefetchGroup(iGroup);
                }
            }
            catch (Exception e)
            {
                Trace.TraceError("Failed to refresh group id:{0}, dump:{1}, exception:{2}", iGroup.Id, iGroup, e);
            }
        }

        private void RefetchGroup(group iGroup)
        {
            Trace.TraceInformation("Fetching stations list for group {0}", iGroup.Name);
            //this local variable temporarily tracks which stations have been refreshed in this cycle
            //to be able to mark those stations in DB which have not been refreshed and delete them
            var aStationsPreserveList = new DataExpirationManager();

            //TODO: rewrite it to make it lazy
            var aStationsRetriever = new StationListWebRetriever();
            aStationsRetriever.ParseXmlStationsList(XmlReader.Create("http://www.rozhlas.cz/iradio/config/radioconfig.xml"));
            var aCurrentFilter = iGroup.Filter;
            foreach (var aStation in aStationsRetriever.StationsList.Where(
                aCurrentStation =>
                    (aCurrentStation != null && Regex.IsMatch(aCurrentStation.Type, aCurrentFilter))))
            {
                RefreshStationDescription(aStation, iGroup);
                //Mark this station as not-to-prune
                aStationsPreserveList.SetValidUntil(aStation.WebUid, DateTime.UtcNow.AddDays(1));
            }

            PruneDeprecatedStations(iGroup, aStationsPreserveList);
            _expiration.SetValidUntil(iGroup.Name, DateTime.UtcNow.AddHours(2));
        }

        private void PruneDeprecatedStations(group iGroup, DataExpirationManager iStationsPreserveList)
        {
            foreach (var iterStation in iGroup.stations)
            {
                if (iStationsPreserveList.IsExpired(iterStation.WebUid))
                {
                    Trace.TraceInformation("Mark for deletion station:{0}, dump:{1}", iterStation.Id, iterStation);
                    _context.stations.DeleteOnSubmit(iterStation);
                }
            }
        }

        private void RefreshAllSchedules()
        {
            foreach (var iterStation in _context.stations)
            {
                RefreshStationSchedule(iterStation);
            }
        }

        private void RefreshStationSchedule(station iterStation)
        {
            try
            {
                if (_expiration.IsExpired(iterStation.WebUid))
                {
                    Trace.TraceInformation("Fetching station schedule {0}", iterStation.WebUid);
                    RetrieveStationSchedule(iterStation);
                    UpdateStationDescriptionExpirationTime(iterStation);
                }
            }
            catch (Exception e)
            {
                Trace.TraceError("Failed to refresh station schedule {0} {1} {2}", iterStation.Id, iterStation, e);
            }
        }

        private void UpdateStationDescriptionExpirationTime(station iterStation)
        {
            var aNow = DateTime.UtcNow;
            //Some stations never have any schedule, so mark them as expiring once in 15 minutes
            var aExpirationTime = iterStation.schedules.Any(sch => sch.StopTime > aNow)
                ? (from sch in iterStation.schedules select sch.StopTime.AddSeconds(-30)).Where(time => time > aNow).Min()
                : aNow.AddMinutes(15);
            _expiration.SetValidUntil(iterStation.WebUid, aExpirationTime);
            Trace.TraceInformation("Station schedule for {0} is valid until {1}", iterStation.ShortName, aExpirationTime);
        }

        private void RetrieveStationSchedule(station iterStation)
        {
            var aShowsRetriever = new ShowsRetriever();
            var aParserState = new ShowsRetriever.ParserState
            {
                //TODO: remove usage of observable collection from model
                Shows = new ObservableCollection<RadioShow>()
            };
            aShowsRetriever.ParseXmlShowsList(from source
                in iterStation.schedulesources
                                              select source.Uri, aParserState);

            SyncStationSchedule(aParserState, iterStation);
        }

        private void SyncStationSchedule(ShowsRetriever.ParserState aParserState, station iterStation)
        {
            foreach (var aShow in aParserState.Shows)
            {
                var show = aShow;
                show.StartTime = TimeZoneInfo.ConvertTimeToUtc(show.StartTime, _crawledScheduleTimeZone);
                show.StopTime = TimeZoneInfo.ConvertTimeToUtc(show.StopTime, _crawledScheduleTimeZone);

                if (iterStation.schedules.All(persistentShow => persistentShow.StopTime != show.StopTime))
                {
                    var aPersistentSchedule = new schedule
                    {
                        Description = show.Description,
                        StartTime = show.StartTime,
                        StopTime = show.StopTime,
                        station = iterStation,
                        stationId = iterStation.Id,
                        Title = show.Title
                    };
                    iterStation.schedules.Add(aPersistentSchedule);
                    Trace.TraceInformation("Mark show for insertion to {0} : {1}", iterStation.WebUid, Printable(aPersistentSchedule));
                    //_context.schedules.InsertOnSubmit(aPersistentSchedule);
                }
                //TODO: support shows schedule update on-the fly (split/rename leads to duplication of the show now)
                //We don't delete stale shows, since we will need them later, and a special batch job will prune them
            }
        }

        private static string Printable(schedule iSchedule)
        {
            var aRetVal = string.Format("({0}:{1}:{2})", iSchedule.StartTime, iSchedule.StopTime, iSchedule.Title);
            return aRetVal;
        }

        private void RefreshStationDescription(StationInfo aStation, group iterGroup)
        {
            station aPersistentStation = null;
            foreach(var aExistingStationsToRefresh in 
                from iterStation in iterGroup.stations
                where iterStation.Id == aStation.Uid
                select iterStation)
            {
                aPersistentStation = aExistingStationsToRefresh;
                _expiration.Invalidate(aPersistentStation.WebUid);
                break;
            }
            //If it is still null, then, probably, it have not been found
            if(aPersistentStation == null)
            {
                aPersistentStation = new station
                {
                    Id = aStation.Uid,
                };
                Trace.TraceInformation("Mark station for insertion {0} {1}", aPersistentStation.WebUid, aPersistentStation);
                _context.stations.InsertOnSubmit(aPersistentStation);
            }

            aPersistentStation.Description = aStation.Description;
            aPersistentStation.ShortName = aStation.Title;
            aPersistentStation.ImagePath = aStation.ImagePath;
            aPersistentStation.GroupId = iterGroup.Id;
            aPersistentStation.WebUid = aStation.WebUid;


            SyncScheduleSources(aStation, aPersistentStation);
            SyncAudioStreams(aStation, aPersistentStation);
        }

        private DataExpirationManager SyncAudioStreams(StationInfo aStation, station aPersistentStation)
        {
            var aStreamExistenceConfirmed = new DataExpirationManager();
            foreach (var aStream in aStation.Streams)
            {
                if (aPersistentStation.AudioStreams.All(iterStream => iterStream.Uri != aStream.Uri))
                {
                    //Add a stream only if it is not yet registered for this station.
                    //TODO: remove stale added streams
                    aPersistentStation.AudioStreams.Add(
                        new AudioStream
                        {
                            AudioFrequency = aStream.Frequency,
                            Bitrate = aStream.Bitrate,
                            Container = aStream.CodecType,
                            StationId = aPersistentStation.Id,
                            Stereo = aStream.Stereo,
                            Vbr = aStream.Vbr,
                            Uri = aStream.Uri
                        });
                }
                aStreamExistenceConfirmed.SetValidUntil(aStream.Uri, DateTime.UtcNow.AddYears(1));
            }
            foreach (var iterAudioStream in aPersistentStation.AudioStreams)
            {
                if (aStreamExistenceConfirmed.IsExpired(iterAudioStream.Uri))
                {
                    Trace.TraceInformation("Mark for deletion {0} {1}", iterAudioStream.Uri, iterAudioStream);
                    _context.AudioStreams.DeleteOnSubmit(iterAudioStream);
                }
            }
            return aStreamExistenceConfirmed;
        }

        private DataExpirationManager SyncScheduleSources(StationInfo aStation, station aPersistentStation)
        {
            var aScheduledSourcesExistenceConfirmed = new DataExpirationManager();
            foreach (var aScheduleSource in aStation.Schedule.Uris)
            {
                if (
                    aPersistentStation.schedulesources.All(
                        iterSchedule => iterSchedule.Uri != aScheduleSource))
                {
                    //Add a source only if it is not yet registered for this station.
                    //TODO: remove stale sources
                    aPersistentStation.schedulesources.Add(
                        new schedulesource
                        {
                            station = aPersistentStation,
                            stationId = aPersistentStation.Id,
                            Uri = aScheduleSource
                        });
                }
                aScheduledSourcesExistenceConfirmed.SetValidUntil(aScheduleSource, DateTime.UtcNow.AddDays(1));
            }
            foreach (var iterScheduleSource in aPersistentStation.schedulesources)
            {
                if (aScheduledSourcesExistenceConfirmed.IsExpired(iterScheduleSource.Uri))
                {
                    Trace.TraceInformation("Mark for deletion {0} {1}", iterScheduleSource.Uri, iterScheduleSource);
                    _context.schedulesources.DeleteOnSubmit(iterScheduleSource);
                }
            }
            return aScheduledSourcesExistenceConfirmed;
        }

        public override void OnStop()
        {
            Trace.TraceInformation("Gracefully stopped application ");
            base.OnStop();
        }
    }
}
